#ifndef __MATRIZ__
#define __MATRIZ__
#include <cassert>
#include <iostream>
#include <cstdlib>
#include <math.h>
using namespace std;

class Matriz {
    public:

    /**
     * Constructores
     */
    Matriz();
    Matriz(const unsigned int);
    Matriz(const Matriz&);
    ~Matriz();

    /**
     *operadores
     */

    /** deja en este parametro la suma de este con el otro */
    void suma(const Matriz&);
    /** deja en este parametro la resta de este con el otro */
    void resta(const Matriz&);
    /** devuelve el vector de largo dim del resultado de la multiplicacion*/
    void producto(const Matriz&);
    /** deja en esta matriz el resultado del producto por un escalar*/
    void producto(const double);
    /** Pongo los nombres de las var xq es mas delcarativo en este caso
        lo que hace es calcualr Ax=b modificando x y devuelve true si pudo */
    bool resolver(const double* b, double* x);

    friend Matriz operator+(const Matriz&, const Matriz&);
    friend Matriz operator-(const Matriz&, const Matriz&);
    friend Matriz operator*(const Matriz&, const Matriz&);
    friend Matriz operator*(const double&, const Matriz&);
    friend Matriz operator*(const Matriz&, const double&);

    /**
     * getters
     */

    /** devuelve un id unico corresdiente a la posicion del elemento en la matriz */
    unsigned int getId(unsigned int, unsigned int) const;
    double operator()(unsigned int, unsigned int) const;
    double operator()(unsigned int) const;
    unsigned int size() const;

    /**
     * setters
     */
    void setPos(unsigned int, unsigned int, double);
    void setPosInId(unsigned int, double);

    /**
     * comparaciones y asignacion
     */
    bool operator==(const Matriz&) const;
    bool operator!=(const Matriz&) const;
    Matriz& operator=(const Matriz&);

    private:
    unsigned int dim;
    double * matriz;

    void swapRows(unsigned int i, unsigned int j);
};

ostream& operator<<(ostream& out, const Matriz& m) {
    for(unsigned int i=0;i<m.size();i++) {
        for(unsigned int j=0;j<m.size();j++) {
            out << m(i,j) << '\t';
        }
        out << endl;
    }
    return out;
}

/***********************************
 * CONSTRUCTORES ******************
 *********************************/

Matriz::Matriz(){
    this->matriz = NULL;
    this->dim = 0;
}

Matriz::Matriz(unsigned int dim){
    assert(dim > 0);
    this->dim = dim;
    unsigned int tamanio = dim*dim;
    this->matriz = new double[tamanio];
    //inicializar en 0.
    for(unsigned int i=0;i<tamanio;i++)
    	this->matriz[i] = 0;

}

Matriz::Matriz(const Matriz& otra){
    if(this != &otra){
        this->dim = otra.dim;
        this->matriz = new double[this->dim*this->dim];
        for(unsigned int x = 0; x < this->dim*this->dim; x++){
            this->matriz[x] = otra.matriz[x];
        }
    }
}

Matriz::~Matriz(){
    if(this->matriz != NULL)
        delete [] this->matriz;
    this->matriz = NULL;
}

/***********************************
* OPERADORES         ***************
************************************/

/** deja en este parametro la suma de este con el otro */
void Matriz::suma(const Matriz& otra){
    assert(size()==otra.size());
    for(unsigned int i=0;i<size();i++)
    	for(unsigned int j=0;j<size();j++)
    		this->setPos(i,j, (*this)(i,j)+otra(i,j) );
}

/** deja en este parametro la resta de este con el otro */
void Matriz::resta(const Matriz& otra){
    assert(this->size()==otra.size());
    for(unsigned int i=0;i<this->size();i++)
    	for(unsigned int j=0;j<this->size();j++)
    		this->setPos(i,j, (*this)(i,j)-otra(i,j));
}

/** devuelve el vector de largo dim del resultado de la multiplicacion*/
void Matriz::producto(const Matriz& otra){
    assert(this->size()==otra.size());
    for(unsigned int i=0;i<size();i++) {
        //guardo la fila nueva tmpmente para no sobreescribri nada
    	double aux[size()];
    	for(unsigned int j=0;j<size();j++) {
	    	double val = 0;
	    	for(unsigned int k=0;k<size();k++)
	    		val += ( (*this)(i,k) * otra(k,j) );
    		aux[j] = val;
    	}
    	for(unsigned int j=0;j<size();j++)
    		this->setPos(i,j,aux[j]);
    }
}

/** deja en esta matriz el resultado del producto por un escalar*/
void Matriz::producto(const double lambda){
    for(unsigned int i=0;i<this->size();i++)
    	for(unsigned int j=0;j<this->size();j++)
    		this->setPos(i,j, (*this)(i,j)*lambda);
}

bool Matriz::resolver(const double* be, double* x) {
    //se copian los valores para modificarse
    Matriz aux = (*this);
    double b[this->size()];
    for(unsigned int i=0;i<this->size();i++)
        b[i]=be[i];
    unsigned int i=0,j=0;
    //1)Triangulamos la matriz junto con b
    while(i<aux.size() && j<aux.size()) {
        //1.1)Hacemos el pivoteo parcial
        unsigned int maxRow = i;
        for(unsigned int k=i+1;k<aux.size();k++) {
            if(fabs(aux(k,j))>fabs(aux(maxRow,j))) {
                maxRow=k;
            }
        }
        if(aux(maxRow,j)==0) return false;
        //1.2)Cambiamos la fila por la de maximo valor en la columna actual
        aux.swapRows(i,maxRow);
        double tmp=b[i];
        b[i]=b[maxRow];
        b[maxRow]=tmp;
        //1.3)Hacemos el siguiente paso de la triangulacion
        for(unsigned int k=i+1;k<aux.size();k++) {
            double valAux=aux(k,j)/aux(i,j);
            for(unsigned int l=j;l<aux.size();l++) {
                if(l==j) {
                    aux.setPos(k,l,0);
                } else
                    aux.setPos(k,l,aux(k,l)-valAux*aux(i,l));
            }
            b[k]=b[k]-valAux*b[i]; //tmb en b!
        }
        i++;
        j++;
    }
    //Resolvemos la matriz ya triangulada
    for(int k=aux.size()-1;k>=0;--k) {
        double val=b[k];
        for(unsigned int l=k+1;l<aux.size();l++) {
            val-= ( aux(k,l)*x[l] );
        }
        x[k]=val/aux(k,k);
    }
    //esto de aca de va a borrar
    /*cout << "LA MATRIZ QUEDA COMO: " << endl;
    for(unsigned int k=0;k<aux.size();k++) {
        for(unsigned int l=0;l<aux.size();l++) {
            cout << aux(k,l) << '\t';
        }
        cout << endl;
    }
    cout << "Y LA SOLUCION ES: [";
    for(unsigned int k=0;k<aux.size();k++)
        cout << x[k] << ", ";
    cout << "]" << endl;*/
    return true;
}

Matriz operator+(const Matriz& a, const Matriz& b){
    assert(a.size() == b.size());
    Matriz res(a);
    res.suma(b);
    return res;
}

Matriz operator-(const Matriz& a, const Matriz& b){
    assert(a.size() == b.size());
    Matriz res(a);
    res.resta(b);
    return res;
}

Matriz operator*(const double& lambda, const Matriz& a){
    Matriz res(a);
    res.producto(lambda);
    return res;
}


Matriz operator*(const Matriz& a, const double& lambda){
    return lambda*a;
}

Matriz operator*(const Matriz& a, const Matriz& b){
    assert(a.size() == b.size());
    Matriz res(a);
    res.producto(b);
    return res;
}

/**********************************
* GETTERS    **********************
**********************************/
unsigned int Matriz::getId(unsigned int fila, unsigned int columna) const {
    return fila*this->size()+columna;
}

/** devuelve el valor en la posicion (fila,columna) */
double Matriz::operator()(unsigned int fila,unsigned int columna) const{
    assert(fila < this->dim && columna < dim);
    return this->matriz[fila*this->dim + columna];
}

double Matriz::operator()(unsigned int id) const{
    return this->matriz[id];
}


/** devuelve el tamanio de la matriz */
unsigned int Matriz::size() const{
    return this->dim;
}

/**********************************
* SETTERS    **********************
**********************************/
/** setea el valor en la posicion (fila,columna) */
void Matriz::setPos(unsigned int fila, unsigned int columna, double valor) {
    assert(fila < this->dim && columna < dim);
    this->matriz[fila*this->dim + columna] = valor;
}

void Matriz::setPosInId(unsigned int id, double valor) {
	this->matriz[id]=valor;
}

/*********************************
* COMPARACION Y ASIGNACIONES *****
*********************************/
bool Matriz::operator==(const Matriz& other) const {
    if( this != &other) {
        if(this->size() != other.size()) {
            return false;
        } else {
            bool igual = true;
            for(unsigned int i = 0; i < this->size() && igual; i++) {
                for(unsigned int j = 0; j < this->size() && igual; j++) {
                    igual = igual && (*this)(i,j) == other(i,j);
                }
            }
            return igual;
        }
    }
    return true;
}

bool Matriz::operator!=(const Matriz& other) const{
    return !((*this) == other);
}

Matriz& Matriz::operator=(const Matriz& other){
    if( this != &other) {
        this->dim = other.size();
        if( this->matriz != NULL)
            delete [] this->matriz;
        this->matriz = new double[this->dim*this->dim];
        for(unsigned int x = 0; x < this->dim*this->dim; x++)
            this->matriz[x] = other.matriz[x];
    }
    return *this;
}

void Matriz::swapRows(unsigned int i, unsigned int j) {
    assert(i<this->size() && j<this->size());
    if(i!=j) {
        for(unsigned int k=0;k<this->size();k++) {
            double tmp = (*this)(i,k);
            setPos(i,k,(*this)(j,k));
            setPos(j,k,tmp);
        }
    }
}

#endif
